﻿using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using Odyssey.Controls.Primitives;
using Odyssey.Utils;
using System.Linq;
using Odyssey.Controls.Models;
using System.Windows.Threading;
using System.Windows.Data;
using Odyssey.Controls.Internals;
using System.Windows.Navigation;

namespace Odyssey.Controls
{
    /// <summary>
    /// Improved ListBox in terms of performance with jump list capability.
    /// OdcListBox supports data items which implement IQuickJumpItem in order to use HeaderItemTemplate.
    /// Tip: If possible, use ItemContainerStyle.Template instead of ItemTemplate because it renders faster since the default OdcListBoxItem template is too generic.
    /// </summary>
    public class OdcListBox : Control, IItemsControl, ICollectionChangedListener
    {
        #region fields

        /// <summary>
        /// The alphabetical quick jump list can be reused in other pages, therefore it is a static control in order to minimize performance impact.
        /// </summary>
        private static Overlay alphabeticQuickJumpList;
        private static Overlay longQuickJumpList;

        private Overlay overlay;
        private int selectedIndex = -1;
        private Queue<OdcListBoxItem> recycledItems = new Queue<OdcListBoxItem>();
        private Dictionary<int, OdcListBoxItem> materializedItems = new Dictionary<int, OdcListBoxItem>();
        private WeakCollectionChangedListener collectionChangedListener;
        private OdcVirtualizingPanel itemsPanel;

        private Queue<OdcListBoxItem> recycledHeaders = new Queue<OdcListBoxItem>();
        private Dictionary<int, OdcListBoxItem> materializedHeaders = new Dictionary<int, OdcListBoxItem>();

        #endregion
        #region ctor

        public OdcListBox()
            : base()
        {
            base.DefaultStyleKey = typeof(OdcListBox);
        }


        #endregion
        #region props


        /// <summary>
        /// Gets or sets whether the quick jump list is opened.
        /// </summary>
        public bool IsJumpListOpen
        {
            get { return (bool)GetValue(IsJumpListOpenProperty); }
            set { SetValue(IsJumpListOpenProperty, value); }
        }

        // Using a DependencyProperty as the backing store for IsJumpListOpen.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty IsJumpListOpenProperty =
            DependencyProperty.Register("IsJumpListOpen", typeof(bool), typeof(OdcListBox), new PropertyMetadata(false, OnJumpListOpenedChanged));

        private static void OnJumpListOpenedChanged(DependencyObject o, DependencyPropertyChangedEventArgs args)
        {
            bool isOpen = (bool)args.NewValue;
            (o as OdcListBox).OnJumpListOpenedChanged(isOpen);
        }

        protected virtual void OnJumpListOpenedChanged(bool isOpen)
        {
            if (isOpen) OpenQuickJumpList(); else CloseQuickJumpList();
        }


        /// <summary>
        /// Invoked when an item is clicked.
        /// </summary>
        public ICommand ItemCommand
        {
            get { return (ICommand)GetValue(ItemCommandProperty); }
            set { SetValue(ItemCommandProperty, value); }
        }

        public static readonly DependencyProperty ItemCommandProperty =
            DependencyProperty.Register("ItemCommand", typeof(ICommand), typeof(OdcListBox), new PropertyMetadata(null));

        /// <summary>
        /// Gets or sets the quick jump mode.
        /// </summary>
        public QuickJumpMode QuickJumpMode
        {
            get { return (QuickJumpMode)GetValue(QuickJumpModeProperty); }
            set { SetValue(QuickJumpModeProperty, value); }
        }

        public static readonly DependencyProperty QuickJumpModeProperty =
            DependencyProperty.Register("QuickJumpMode", typeof(QuickJumpMode), typeof(OdcListBox), new PropertyMetadata(QuickJumpMode.Alphabetical));

        /// <summary>
        /// Gets or sets whether each item of the list becomes visible in an interval one by one. Otherwise all items are initialitely shown at once, which might be slower.
        /// The default value is true.
        /// </summary>
        public bool IsIntervalEnabled
        {
            get { return (bool)GetValue(IsItemIntervalEnabledProperty); }
            set { SetValue(IsItemIntervalEnabledProperty, value); }
        }

        public static readonly DependencyProperty IsItemIntervalEnabledProperty =
            DependencyProperty.Register("IsIntervalEnabled", typeof(bool), typeof(OdcListBox), new PropertyMetadata(true));

        /// <summary>
        /// Gets or sets the Width of each Item. The default value is NaN.
        /// Note that the virtualizing panel must not necassarily support this property.
        /// </summary>
        public double ItemWidth
        {
            get { return (double)GetValue(ItemWidthProperty); }
            set { SetValue(ItemWidthProperty, value); }
        }

        public static readonly DependencyProperty ItemWidthProperty =
            DependencyProperty.Register("ItemWidth", typeof(double), typeof(OdcListBox), new PropertyMetadata(double.NaN));

        /// <summary>
        /// Gets or sets the height of each item. The default value is 80.
        /// Note that the virtualizing panel must not necassarily support this property.
        /// </summary>
        public double ItemHeight
        {
            get { return (double)GetValue(ItemHeightProperty); }
            set { SetValue(ItemHeightProperty, value); }
        }

        public static readonly DependencyProperty ItemHeightProperty =
            DependencyProperty.Register("ItemHeight", typeof(double), typeof(OdcListBox), new PropertyMetadata(80d));

        /// <summary>
        /// Gets or sets the DataTemplate for an a header item. This a template for an IQuickJumpItem which as set IsHeader to true.
        /// </summary>
        public DataTemplate HeaderTemplate
        {
            get { return (DataTemplate)GetValue(HeaderTemplateProperty); }
            set { SetValue(HeaderTemplateProperty, value); }
        }

        public static readonly DependencyProperty HeaderTemplateProperty =
            DependencyProperty.Register("HeaderTemplate", typeof(DataTemplate), typeof(OdcListBox), new PropertyMetadata(null));

        /// <summary>
        /// Gets or sets the DataTemplate for an item.
        /// </summary>
        public DataTemplate ItemTemplate
        {
            get { return (DataTemplate)GetValue(ItemTemplateProperty); }
            set { SetValue(ItemTemplateProperty, value); }
        }

        public static readonly DependencyProperty ItemTemplateProperty =
            DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(OdcListBox), new PropertyMetadata(null));


        public Style HeaderContainerStyle
        {
            get { return (Style)GetValue(HeaderContainerStyleProperty); }
            set { SetValue(HeaderContainerStyleProperty, value); }
        }

        public static readonly DependencyProperty HeaderContainerStyleProperty =
            DependencyProperty.Register("HeaderContainerStyle", typeof(Style), typeof(OdcListBox), new PropertyMetadata(null));


        /// <summary>
        /// Gets or sets the style for the ItemContainer. Which in this case is of type OdcListBoxItem.
        /// </summary>
        public Style ItemContainerStyle
        {
            get { return (Style)GetValue(ItemContainerStyleProperty); }
            set { SetValue(ItemContainerStyleProperty, value); }
        }

        public static readonly DependencyProperty ItemContainerStyleProperty =
            DependencyProperty.Register("ItemContainerStyle", typeof(Style), typeof(OdcListBox), new PropertyMetadata(null));

        /// <summary>
        /// Gets or sets the selected data item.
        /// </summary>
        public object SelectedItem
        {
            get { return (object)GetValue(SelectedItemProperty); }
            set { SetValue(SelectedItemProperty, value); }
        }

        public static readonly DependencyProperty SelectedItemProperty =
            DependencyProperty.Register("SelectedItem", typeof(object), typeof(OdcListBox), new PropertyMetadata(null, OnSelectionChanged));

        private static void OnSelectionChanged(DependencyObject o, DependencyPropertyChangedEventArgs args)
        {
            (o as OdcListBox).OnSelectionChanged(args.OldValue, args.NewValue);
        }


        /// <summary>
        /// Gets or sets the source of the items.
        /// </summary>
        public IList ItemsSource
        {
            get { return (IList)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }

        public static readonly DependencyProperty ItemsSourceProperty =
            DependencyProperty.Register("ItemsSource", typeof(IList), typeof(OdcListBox), new PropertyMetadata(null, OnItemsSourceChanged));

        private static void OnItemsSourceChanged(DependencyObject o, DependencyPropertyChangedEventArgs args)
        {
            (o as OdcListBox).OnItemsSourceChanged(args.OldValue as IList, args.NewValue as IList);
        }

        #endregion
        #region methods

        private void ScrollSelectedItemIntoView()
        {
            if (SelectedItem != null && ItemsSource != null)
            {
                int index = ItemsSource.IndexOf(SelectedItem);
                ScrollIntoView(index);
            }
        }

        protected virtual void OnSelectionChanged(object oldValue, object newValue)
        {
            if (ItemsSource != null)
            {
                if (oldValue != null)
                {
                    int oldIndex = selectedIndex;
                    if (oldIndex >= 0)
                    {
                        var selectedItem = GetMaterializedItem(oldIndex);
                        if (selectedItem != null) selectedItem.IsSelected = false;
                    }
                }
                if (newValue != null)
                {
                    int newIndex = ItemsSource.IndexOf(newValue);
                    if (newIndex >= 0)
                    {
                        var selectedItem = GetMaterializedItem(newIndex);
                        if (selectedItem != null) selectedItem.IsSelected = true;
                    }
                    selectedIndex = newIndex;
                }
                else selectedIndex = -1;
                var eh = SelectionChanged;
                if (eh != null)
                {
                    eh(this, new SelectionChangedEventArgs(GetArray(oldValue), GetArray(newValue)));
                }
            }
        }

        private static IList GetArray(object value)
        {
            if (value == null) return new object[] { };
            return new object[] { value };
        }

        protected object GetObject(int index)
        {
            IList items = ItemsSource;
            if (index < 0 || items == null || items.Count <= index) return null;
            return items[index];
        }

        protected OdcListBoxItem GetMaterializedItem(int index)
        {
            OdcListBoxItem item;
            if (!materializedItems.TryGetValue(index, out item))
            {
                item = null;
            }
            return item;
        }

        protected OdcListBoxItem GetItemContainer(int index, out bool isNew)
        {
            OdcListBoxItem item;
            object obj = GetObject(index);
            if (obj == null)
            {
                isNew = false;
                return null;
            }
            Queue<OdcListBoxItem> recycledItems;
            Dictionary<int, OdcListBoxItem> materializedItems;
            DetermineItemPool(obj, out recycledItems, out materializedItems);

            if (materializedItems.TryGetValue(index, out item))
            {
                isNew = false;
                if (!object.Equals(obj, item.Content))
                {
                    item.Content = obj;
                    item.DataContext = obj;
                }
            }
            else
            {
                isNew = true;

                if (recycledItems.Count > 0)
                {
                    item = recycledItems.Dequeue();
                    item.Content = obj;
                    item.DataContext = obj;
                    item.IsSelected = object.Equals(obj, SelectedItem);
                }
                else
                {

                    DataTemplate template = GetTemplate(obj);
                    item = new OdcListBoxItem
                    {
                        ListBox = this,
                        Style = GetContainerStyle(obj),
                        ContentTemplate = template,
                        Content = obj,
                        DataContext = obj,
                        IsSelected = object.Equals(obj, SelectedItem)
                    };

                }
                materializedItems.Add(index, item);
            }
            return item;
        }

        private Style GetContainerStyle(object obj)
        {
            IGroupItem header = obj as IGroupItem;
            return header != null && header.IsHeader ? HeaderContainerStyle : ItemContainerStyle;
        }

        private DataTemplate GetTemplate(object obj)
        {
            DataTemplate headerTemplate = HeaderTemplate;
            if (headerTemplate == null) return ItemTemplate;

            IGroupItem header = obj as IGroupItem;
            return (header != null && header.IsHeader) ? headerTemplate : ItemTemplate;
        }

        private void DetermineItemPool(object obj, out Queue<OdcListBoxItem> recycledItems, out Dictionary<int, OdcListBoxItem> materializedItems)
        {
            IGroupItem header = HeaderTemplate != null ? obj as IGroupItem : null;

            if (header != null && header.IsHeader)
            {
                recycledItems = this.recycledHeaders;
                materializedItems = this.materializedHeaders;
            }
            else
            {
                recycledItems = this.recycledItems;
                materializedItems = this.materializedItems;
            }
        }

        protected void ReleaseItemContainer(int index)
        {
            if (!ReleaseItem(index, recycledItems, materializedItems))
            {
                ReleaseItem(index, recycledHeaders, materializedHeaders);
            }
        }

        private bool ReleaseItem(int index, Queue<OdcListBoxItem> recycledItems, Dictionary<int, OdcListBoxItem> materializedItems)
        {
            OdcListBoxItem item;
            if (materializedItems.TryGetValue(index, out item))
            {
                materializedItems.Remove(index);
                item.IsSelected = false;
                item.Content = null;
                item.DataContext = null;
                recycledItems.Enqueue(item);
                return true;
            }
            else return false;
        }

        private void OnItemsSourceChanged(IList oldValue, IList newValue)
        {
            SelectedItem = null;
            if (collectionChangedListener != null)
            {
                collectionChangedListener.Disconnect();
                collectionChangedListener = null;
            }
            INotifyCollectionChanged newCol = newValue as INotifyCollectionChanged;
            if (newCol != null)
            {
                collectionChangedListener = WeakCollectionChangedListener.CreateIfNecessary(newCol, this);
            }
            if (itemsPanel != null)
            {
                //                itemsPanel.Invalidate();
                this.itemsPanel.Reset();
            }
            OnItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));

            //this.recycledHeaders.Clear();
            //this.recycledItems.Clear();
            //this.materializedHeaders.Clear();
            //this.materializedItems.Clear();

        }

        protected virtual void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            //    SelectedItem = null;
        }

        public override void OnApplyTemplate()
        {
            itemsPanel = null;
            base.OnApplyTemplate();

            Dispatcher.BeginInvoke(() =>
                {
                    itemsPanel = this.GetVisualDescendants().OfType<OdcVirtualizingPanel>().FirstOrDefault();
                    ScrollSelectedItemIntoView();
                });
        }

        /// <summary>
        /// Scrolls the specified item into view if not already present.
        /// </summary>
        /// <param name="itemIndex">index of the item to scroll into view.</param>
        public void ScrollIntoView(int itemIndex)
        {
            if (itemsPanel != null)
            {
                itemsPanel.ScrollIntoView(itemIndex);
            }
        }

        public void SetQuickJumpIndex(int itemIndex)
        {
            ScrollViewer scrollViewer = this.GetScrollViewer();
            if (scrollViewer != null)
            {
                double offset = ItemHeight * itemIndex;
                scrollViewer.ScrollToVerticalOffset(offset);
            }
        }

        /// <summary>
        /// Opens the quick jump list.
        /// </summary>
        protected virtual void OpenQuickJumpList()
        {
            bool showsItems = false;
            switch (QuickJumpMode)
            {
                case Controls.QuickJumpMode.Alphabetical:
                    showsItems = OpenAlphabeticalQuickJumpList();
                    break;

                case Controls.QuickJumpMode.List:
                    showsItems = OpenLongQuickJumpList();
                    break;
            }
            if (!showsItems) this.IsJumpListOpen = false;
        }

        protected virtual void CloseQuickJumpList()
        {
            if (overlay != null)
            {
                overlay.IsOpen = false;
            }
        }



        protected virtual bool OpenAlphabeticalQuickJumpList()
        {
            if (this.ItemsSource == null || this.ItemsSource.Count == 0) return false;
            QuickJumpViewModel model = new QuickJumpViewModel();
            model.UpdateFromItemsSource(this.ItemsSource);
            if (model.Items.Count > 0)
            {
                AlphabeticalQuickJumpList overlay = alphabeticQuickJumpList as AlphabeticalQuickJumpList;
                if (overlay == null) overlay = new AlphabeticalQuickJumpList(this);
                overlay.nestedElement = this;
                model.ListBox = this;
                model.Overlay = overlay;

                overlay.Closing += new EventHandler(model.HandleQuickJumpListClosing);
                overlay.Closed += model.HandleQuickJumpListClosed;
                overlay.Content = model;
                alphabeticQuickJumpList = overlay;
                this.overlay = overlay;
                overlay.IsOpen = true;
                return true;
            }
            else return false;
        }

        protected virtual bool OpenLongQuickJumpList()
        {
            if (this.ItemsSource == null || this.ItemsSource.Count == 0) return false;
            LongQuickJumpViewModel model = new LongQuickJumpViewModel();
            model.UpdateFromItemsSource(this.ItemsSource);

            if (model.Items.Count > 0)
            {
                LongQuickJumpList overlay = longQuickJumpList as LongQuickJumpList;
                if (overlay == null)
                {
                    overlay = new LongQuickJumpList(this);
                    longQuickJumpList = overlay;
                }
                overlay.nestedElement = this;

                model.Overlay = overlay;
                model.ListBox = this;

                overlay.Closing += new EventHandler(model.HandleQuickJumpListClosing);
                overlay.Closed += model.HandleQuickJumpListClosed;
                overlay.Content = model;
                this.overlay = overlay;
                overlay.IsOpen = true;
                return true; 
            }
            else return false;
        }

        public void ItemClick(OdcListBoxItem listBoxItem)
        {
            ICommand command = ItemCommand;
            if (command != null)
            {
                command.Execute(listBoxItem.CommandParameter ?? listBoxItem.DataContext);
            }
            var eh = ItemInvoked;
            if (eh != null) eh(this, EventArgs.Empty);
        }


        #endregion
        #region IItemsControls

        FrameworkElement IItemsControl.GetItemContainer(int index, out bool isNew)
        {
            return GetItemContainer(index, out isNew);
        }

        void IItemsControl.ReleaseItemContainer(int index)
        {
            ReleaseItemContainer(index);
        }

        void IItemsControl.ItemContainerAttached(int index, FrameworkElement item)
        {
            var data = GetObject(index);

            ContentControl cc = item as ContentControl;
            cc.Content = data;
            cc.DataContext = data;
        }

        #endregion
        #region ICollectionChangedListener

        void ICollectionChangedListener.OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            OnItemsChanged(e);
            ICollectionChangedListener listener = this.itemsPanel as ICollectionChangedListener;
            if (listener != null) listener.OnCollectionChanged(this, e);
        }

        #endregion
        #region events

        public event SelectionChangedEventHandler SelectionChanged;
        public event EventHandler ItemInvoked;

        #endregion

    }
}
